/*
 * Copyright (c) 2018-2024 北京华恒盛世科技有限公司
 * QUICKPOOL is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *     http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
*/

#include "yaml.h"
#include "common.h"
#include "channel.h"
#include "quickpool.h"
#include "lib.h"

struct config_param commandParams[] = {
    {"CONFDIR", STRING_PARAM, -1, {NULL}},
    {"DEBUG_CMD", STRING_PARAM, -1, {NULL}},
    {"TIME_DEBUG_CMD", INT_PARAM, -1, {NULL}},
    {"WORKDIR", STRING_PARAM, -1, {NULL}},
    {"SHAREDIR", STRING_PARAM, -1, {NULL}},
    {"SERVERDIR", STRING_PARAM, -1, {NULL}},
    {"MASTER_LIST", STRING_PARAM, -1, {NULL}},
    {"JM_PORT", INT_PARAM, -1, {0}},
    {"JS_PORT", INT_PARAM, -1, {0}},
    {"CMD_LOGDIR", STRING_PARAM, -1, {NULL}},
    {"CMD_LOGMASK", STRING_PARAM, -1, {NULL}},
    {"API_CONNTIMEOUT",INT_PARAM, -1, {0}},
    {"API_RECVTIMEOUT",INT_PARAM, -1, {0}},
    {"API_QUOTE_CMD", STRING_PARAM, -1, {NULL}},
    {"AUTH", STRING_PARAM, -1, {NULL}},
    {"CONNECT_RETRY",INT_PARAM, -1, {0}},
    {NULL, STRING_PARAM, -1, {NULL}},
};

struct config_param baseParams[] =
{
    {"CONFDIR", STRING_PARAM, -1, {NULL}},
    {"SERVERDIR", STRING_PARAM, -1, {NULL}},
    {"LIBDIR", STRING_PARAM, -1, {NULL}},
    {"BINDIR", STRING_PARAM, -1, {NULL}},
    {"MASTER_LIST", STRING_PARAM, -1, {NULL}},
    {"EE_DEBUG", STRING_PARAM, -1, {NULL}},
    {"EE_PORT", INT_PARAM, -1, {0}},
    {"EE_TIME_DEBUG", INT_PARAM, -1, {0}},
    {"JM_DEBUG", STRING_PARAM, -1, {NULL}},
    {"JM_PORT", INT_PARAM, -1, {0}},
    {"JM_TIME_DEBUG", INT_PARAM, -1, {0}},
    {"JS_DEBUG", STRING_PARAM, -1, {NULL}},
    {"JS_PORT", INT_PARAM, -1, {0}},
    {"JS_TIME_DEBUG", INT_PARAM, -1, {0}},
    {"STRIP_DOMAIN", STRING_PARAM, -1, {NULL}},
    {"LOG_MASK", STRING_PARAM, -1, {NULL}},
    {"SERVER_HOSTS", STRING_PARAM, -1, {NULL}},
    {"AUTH", STRING_PARAM, -1, {NULL}},
    {"ID_PORT", INT_PARAM, -1, {0}},
    {"API_CONNTIMEOUT", INT_PARAM, -1, {0}},
    {"API_RECVTIMEOUT", INT_PARAM, -1, {0}},
    {"AM_OPTIONS", STRING_PARAM, -1, {NULL}},
    {"TMPDIR", STRING_PARAM, -1, {NULL}},
    {"LOGDIR", STRING_PARAM, -1, {NULL}},
    {"NO_HOSTS_FILE", STRING_PARAM, -1, {NULL}},
    {"WORKDIR", STRING_PARAM, -1, {NULL}},
    {"SHAREDIR", STRING_PARAM, -1, {NULL}},
    {"OL_CGROUP_ROOT", STRING_PARAM, -1, {NULL}},
    {"QP_CHKPNT_ROOT", STRING_PARAM, -1, {NULL}},
    {"QP_CHKPNT_METHOD_DIR", STRING_PARAM, -1, {NULL}},
    {"QP_CHKPNT_METHOD", STRING_PARAM, -1, {NULL}},
    {"LICENSE_FILE", STRING_PARAM, -1, {NULL}},
    {"QP_CUDA_ENABLE", STRING_PARAM, -1, {NULL}},
    {"DEBUG_CMD", STRING_PARAM, -1, {NULL}},
    {"TIME_DEBUG_CMD", INT_PARAM, -1, {NULL}},
    {"CMD_LOGDIR", STRING_PARAM, -1, {NULL}},
    {"CMD_LOGMASK", STRING_PARAM, -1, {NULL}},
    {"CONNECT_RETRY", INT_PARAM, -1, {0}},
    {"CGROUP_MEMORY_ENFORCE", BOOL_PARAM, -1, {FALSE}},
    {NULL, STRING_PARAM, -1, {NULL}}
};

char *gstrTmpDir;

#define DEFAULT_API_CONNTIMEOUT 5
#define DEFAULT_API_RECVTIMEOUT 30
#define DEFAULT_API_RETRIES 3
#define DEFAULT_JS_PORT  7395
#define DEFAULT_JM_PORT  7396

static int envset = FALSE;
char *g_masterlist = NULL; 
int g_conntimeout = DEFAULT_API_CONNTIMEOUT;
int g_recvtimeout = DEFAULT_API_RECVTIMEOUT;
int g_js_port = DEFAULT_JS_PORT;
int g_jm_port = DEFAULT_JM_PORT;
int g_connretries = DEFAULT_API_RETRIES;

static int read_configure_file(struct config_param *, struct config_param *, char *);
static bool_t update_param_fromenv(struct config_param *);
static char *getmy_tmp_dir(void);

/**
 * @brief       Do quickpool initialization.
 *
 * @param[in]   app           #1: name of the caller
 *
 * @retval      0             #1: succeed
 * @retval      -1            #2: failed
 *
 * @note        Read qp.conf file and check parameters.
 *              Do channel initialization.
 *              Open command log if CMD_LOGDIR is defined
 */
int
gf_qp_init(const char *app)
{
    if (envset) {
        return 0;
    }

    if (0 > init_config_param(commandParams, NULL)) {
	return -1;
    }

    if (gf_chan_init() < 0) {
        return -1;
    }

    if (NULL == commandParams[CMD_WORKDIR].string_value
        && NULL == commandParams[CMD_SHAREDIR].string_value) {

        fprintf(stderr, "Thers is no WORKDIR defined in qp.conf file.\n");
	return -1;
    }

    if (NULL == commandParams[CMD_WORKDIR].string_value) {
        commandParams[CMD_WORKDIR].string_value = copy_string(commandParams[CMD_SHAREDIR].string_value);
    }

    envset = TRUE;

    g_connretries = commandParams[CONNECT_RETRY].int_value;
    if (0 >= g_connretries) {
        g_connretries = DEFAULT_API_RETRIES;
    }

    if (NULL == commandParams[CMD_LOGDIR].string_value) { // do not enable log, stop at here.
        return 0;
    }

    char strMyName[MAXHOSTNAMELEN+1];
    gethostname(strMyName, MAXHOSTNAMELEN);

    char logfile[PATH_MAX+1];
    if (NULL == app) {

        snprintf(logfile, PATH_MAX, "%s/command.%s.log",
                 commandParams[CMD_LOGDIR].string_value, strMyName);
        open_log(logfile, commandParams[CMD_LOGMASK].string_value,
                 commandParams[TIMEDEBUG_CMD].int_value);
        gf_set_logclass(commandParams[DEBUG_CMD].string_value);
    } else {

        snprintf(logfile, PATH_MAX, "%s/%s.%s.log",
                 commandParams[CMD_LOGDIR].string_value, app, strMyName);
        open_log(logfile, commandParams[CMD_LOGMASK].string_value,
                 commandParams[TIMEDEBUG_CMD].int_value);
        gf_set_logclass(commandParams[DEBUG_CMD].string_value);
    }

    return 0;
} // end function gf_qp_init

/**
 * @brief       Read qp.conf and set configuration parameter values.
 *
 * @param[out]  param         #1: store configuration parameters
 * @param[in]   pathname      #2: path of the qp.conf file
 *
 * @retval      0             #1: succeed
 * @retval      -1            #2: failed
 *
 * @note        Use pathname if is not NULL
 *              Then use ENVDIR environment varaible if set
 *              Finally use /etc
 */
int
init_config_param(struct config_param *param, char *pathname)
{
    if (NULL == pathname) {

        // pathname is not set, check ENVDIR
        char *envdir;
        envdir = getenv("ENVDIR");
        if (NULL != envdir) { // ENVDIR has value, use it.
            pathname = envdir;
        } else { // ENVDIR is not set, use /etc
            pathname = (char *)SYSCONFDIR;
        }
    }

    if (read_configure_file(baseParams, param, pathname) < 0) {
        return -1;
    } else {
        if (!update_param_fromenv(baseParams)) {
            return -1;
        }

        if (!update_param_fromenv(param)) {
            return -1;
        }
    }

    if (NULL != g_masterlist) {
        return 0;
    }

    if (NULL == baseParams[BASE_CONFDIR].string_value) {
        log_message(ERR, NOCLASS, "%s: CONFDIR must be specified.", __func__);
        return -1;
    }

    if (NULL == baseParams[BASE_SERVERDIR].string_value) {
        log_message(ERR, NOCLASS, "%s: SERVERDIR must be specified.", __func__);
        return -1;
    }

    if (NULL == baseParams[BASE_BINDIR].string_value) {
        char *bindir = getenv("BINDIR");
        if (NULL != bindir) {
            baseParams[BASE_BINDIR].string_value = copy_string(bindir);
        }
    }

    if (NULL == baseParams[BASE_LIBDIR].string_value) {
        char *libdir = getenv("LIBDIR");
        if (NULL != libdir) {
            baseParams[BASE_LIBDIR].string_value = copy_string(libdir);
        }
    }

    if (NULL == baseParams[BASE_WORKDIR].string_value
        && NULL != baseParams[BASE_SHAREDIR].string_value) {
        baseParams[BASE_WORKDIR].string_value = copy_string(baseParams[BASE_SHAREDIR].string_value);
    }

    if (NULL != baseParams[BASE_MASTER_LIST].string_value) {
        g_masterlist = copy_string(baseParams[BASE_MASTER_LIST].string_value);
    } else {
        log_message(ERR, NOCLASS, "%s: MASTER_LIST must be specified.", __func__);
        return -1;
    }

    g_conntimeout = baseParams[BASE_API_CONNTIMEOUT].int_value;
    if (0 >= g_conntimeout) {
       g_conntimeout = DEFAULT_API_CONNTIMEOUT;
    }

    g_recvtimeout = baseParams[BASE_API_RECVTIMEOUT].int_value;
    if (0 >= g_recvtimeout) {
       g_recvtimeout = DEFAULT_API_RECVTIMEOUT;
    }

    if (0 < baseParams[BASE_JS_PORT].int_value) { // JS port is set.
        g_js_port = baseParams[BASE_JS_PORT].int_value;
    } else {

        struct servent *sv;
        sv = getservbyname("js", "tcp");
        if (sv) { // OS has js port defined.
            g_js_port = sv->s_port;
        } else { // not defined anywhere, use default value.
            g_js_port = DEFAULT_JS_PORT;
        }
    }

    if (0 < baseParams[BASE_JM_PORT].int_value) { // JM port is set.
        g_jm_port = baseParams[BASE_JM_PORT].int_value;
    } else {

        struct servent *sv;
        sv = getservbyname("jm", "tcp");
        if (sv) { // OS has jm port defined.
            g_jm_port = sv->s_port;
        } else { // not defined anywhere, use default value.
            g_jm_port = DEFAULT_JM_PORT;
        }
    }

    if (NULL != baseParams[BASE_SERVER_HOSTS].string_value) {

        char *sp;
        for (sp=baseParams[BASE_SERVER_HOSTS].string_value; *sp!='\0'; sp++) {
            if ('\"' != *sp) {
                continue;
            }
            *sp = ' ';
        }
    }    

    getmy_tmp_dir(); // set tmp dir for some temporary file.

    return 0;
} // end function init_config_param

/**
 * @brief       To check if the input name is set in configuration parameters.
 *
 * @param[in]   name          #1: parameter name
 * @param[in]   param         #2: configuration parameters
 *
 * @retval      >= 0          #1: index of the parameter
 * @retval      -1            #2: name is not set
 */
int
match_param_key(const char *name, struct config_param *param)
{
    if (NULL == param) {
        return -1;
    }

    unsigned int i=0;
    for (; param->param_name; param++, i++) {
        if (strcasecmp(param->param_name, name) == 0) {
            return i;
        }
    }

    return -1;
} // end function match_param_key

/**
 * @brief       Set value to configuration parameter.
 *
 * @param[in]   index         #1: index of the parameter
 * @param[in]   value         #2: value of the parameter
 * @param[out]  param         #3: configuration parameters
 *
 * @retval      TRUE          #1: succeed
 * @retval      FALSE         #2: failed, value of the parameter is not valid
 */
bool_t
set_param_value(int index, char *value, struct config_param *param)
{
    if (NULL == param) {
        return TRUE;
    }

    if (0 > index) {
        return FALSE;
    }

    if (NULL == value) {
        value = (char *)"";
    }

    switch (param[index].param_type) {
    case STRING_PARAM:
        FREEUP(param[index].string_value);
        param[index].string_value = copy_string(value);
        param[index].index = index;

        break;
    case INT_PARAM:
        if (!isdigit_string(value)) {
            log_message(WARNING, NOCLASS, "%s: the value <%s> of the key <%s> should be a digit.",
                        __func__, value, param[index].param_name);

            return FALSE;
        } else {
            param[index].int_value = atoi(value);
        }
        param[index].index = index;

        break;
    case FLOAT_PARAM:
        if (!isnumber(value)) {
            log_message(WARNING, NOCLASS, "%s: the value <%s> of the key <%s> should be a digit.",
                        __func__, value, param[index].param_name);

            return FALSE;
        } else {
            param[index].float_value = atof(value);
        }
        param[index].index = index;

        break;
    case BOOL_PARAM:
        if (strcasecmp(value, "y") == 0 || strcasecmp(value, "yes") == 0) {
            param[index].bool_value = TRUE;
        } else if (strcasecmp(value, "n") == 0 || strcasecmp(value, "no") == 0) {
            param[index].bool_value = FALSE;
        } else {
            log_message(WARNING, NOCLASS, "%s: the value <%s> of the key <%s> should be Y or N.",
                        __func__, value, param[index].param_name);

            return FALSE;
        }
        param[index].index = index;

        break;
    }

    return TRUE;
} // end function set_param_value

/**
 * @brief       Reset value of configuration parameters.
 *
 * @param[out]  param         #1: configuration parameters
 *
 * @note        This is usedful for reading parameters
 *              in section several times.
 */
void
reset_param_value(struct config_param *param)
{
    if (NULL == param) {
        return;
    }

    for (; param->param_name; param++) {
        switch (param->param_type) {
        case STRING_PARAM:
            FREEUP(param->string_value);
            break;
        case INT_PARAM:
            param->int_value = 0;
            break;
        case FLOAT_PARAM:
            param->float_value = 0.0;
            break;
        case BOOL_PARAM:
            param->bool_value = FALSE;
            break;
        default:
            break;
        }

        param->index = -1;
    }

    return;
} // end function reset_param_value

/**
 * @brief       free allocated value of parameters.
 *
 * @param[out]  param         #1: configuration parameters
 */
void
free_param_value(struct config_param *param)
{
    if (NULL == param) {
        return;
    }

    for (; param->param_name; param++) {
        switch (param->param_type) {
        case STRING_PARAM:
            FREEUP(param->string_value);
            break;
        default:
            break;
        }
    }

    return;
} // end function free_param_value

/* ------------------------------------------------
 *
 *  below are static functions used in this file
 *
 * ------------------------------------------------ */

/**
 * @brief       Read qp.conf file and set value for parameters.
 *
 * @param[out]  list1         #1: index of the parameter
 * @param[out]  list2         #2: value of the parameter
 * @param[in]   path          #3: configuration parameters
 *
 * @retval      TRUE          #1: succeed
 * @retval      FALSE         #2: failed, value of the parameter is not valid
 */
static int
read_configure_file(struct config_param *list1, struct config_param *list2, char *path)
{
    char filename[PATH_MAX+1];
    FILE *fpConf;

    if (NULL != path) {
        snprintf(filename, PATH_MAX, "%s/qp.conf", path);
        fpConf = fopen(filename, "r");
    } else { // should not go here.
        char *ep = getenv("ENVDIR");

        if (NULL == ep) {
            snprintf(filename, PATH_MAX, "%s/qp.conf", SYSCONFDIR);
            fpConf = fopen(filename, "r");
        } else {

            if (NULL != strstr(ep, "../")) {
                log_message(ERR, NOCLASS, "%s: ENVDIR <%s> cannot have \"../\".",
                            __func__, ep);
                return -1;
            }

            snprintf(filename, PATH_MAX, "%s/qp.conf", ep);
            fpConf = fopen(filename, "r");
        }
    }

    if (NULL == fpConf) { // open qp.conf failed.

        log_message(ERR, NOCLASS, STR_FUNC_S_FAIL_M, __func__, "fopen", filename);
        return -1;
    }

    yaml_parser_t parser;
    if(!yaml_parser_initialize(&parser)) {
        log_message(ERR, NOCLASS, "%s: failed to initialize yaml parser!", __func__);
        return -1;
    }

    yaml_parser_set_input_file(&parser, fpConf);

    yaml_token_t token;
    bool_t key=FALSE;
    char *strKey=NULL;
    int index1 = -1, index2=-1;
    do {

        yaml_parser_scan(&parser, &token);

        if (YAML_NO_ERROR != parser.error) {

            log_message(ERR, NOCLASS, "%s: error occurred when reading conf file(%s) at line %d: %s",
                        __func__, filename, parser.mark.line, parser.problem);

            yaml_parser_delete(&parser);

            return -1;
        }

        switch(token.type) {
        case YAML_KEY_TOKEN:
            key = TRUE;
            break;
        case YAML_VALUE_TOKEN:
            key = FALSE;
            break;
        case YAML_SCALAR_TOKEN:
            if (key) {

                char *strValue = (char *)token.data.scalar.value;
                index1 = match_param_key(strValue, list1);
                index2 = match_param_key(strValue, list2);
                if (0 > index1 && 0 > index2) {
                    log_message(WARNING, NOCLASS, "%s: in file %s at line %d, unknown key(%s) is specified.",
                                __func__, filename, parser.mark.line, strValue);
                    break;
                }

                strKey = copy_string((char *)token.data.scalar.value);
            } else {
                if (NULL != strKey) {
                    if (0 <= index1) {
                        set_param_value(index1, (char *)token.data.scalar.value, list1);
                        index1 = -1;
                    }

                    if (0 <= index2) {
                        set_param_value(index2, (char *)token.data.scalar.value, list2);
                        index2 = -1;
                    }

                    FREEUP(strKey);
                }
            }
            break;
        default:
            key = FALSE;
            break;
        }

        if(token.type != YAML_STREAM_END_TOKEN) {
            yaml_token_delete(&token);
        }
    } while(token.type != YAML_STREAM_END_TOKEN);
    yaml_token_delete(&token);

    yaml_parser_delete(&parser);
    fclose(fpConf);

    return 0;
} // end function read_configure_file

/**
 * @brief       Update value of configuration parameter if environment variable is et.
 *
 * @param[in|out]  param      #1: configuration parameters
 *
 * @retval      TRUE          #1: succeed
 * @retval      FALSE         #2: failed, value of the parameter is not valid
 *
 * @note        Environment variable would cover the value in configuration file.
 */
static bool_t
update_param_fromenv(struct config_param *param)
{
    if (NULL == param) {
        return TRUE;
    }

    unsigned int i=0;
    for (; param->param_name!=NULL; param++, i++) {

        char *strEnv;
        strEnv = getenv(param->param_name);
        if (NULL == strEnv) { // environment variable is not set, ignore it
            continue;
        }

        switch (param->param_type) {
        case STRING_PARAM:
            FREEUP(param->string_value);
            param->string_value = copy_string(strEnv);

            break;
        case INT_PARAM:
            if (!isdigit_string(strEnv)) {
                log_message(WARNING, NOCLASS, "%s: the value <%s> of the key <%s> should be a digit.",
                            __func__, strEnv, param->param_name);
                continue;
            } else {
                param->int_value = atoi(strEnv);
            }

            break;
        case FLOAT_PARAM:
            if (!isnumber(strEnv)) {
                log_message(WARNING, NOCLASS, "%s: the value <%s> of the key <%s> should be a digit.",
                            __func__, strEnv, param->param_name);
                continue;
            } else {
                param->float_value = atof(strEnv);
            }

            break;
        case BOOL_PARAM:
            if (0 == strcasecmp(strEnv, "y") || 0 == strcasecmp(strEnv, "yes")) {
                param->bool_value = TRUE;
            } else if (0 == strcasecmp(strEnv, "n") || 0 == strcasecmp(strEnv, "no")) {
                param->bool_value = FALSE;
            } else {
                log_message(WARNING, NOCLASS, "%s: the value <%s> of the key <%s> should be Y or N.",
                            __func__, strEnv, param->param_name);
                continue;
            }

            break;
        }

        param->index = i;
        break;
    }

    return TRUE;
} // end function update_param_fromenv

static char *
getmy_tmp_dir(void)
{
    char *tmpSp = NULL;
    struct stat stb;

    if (NULL != gstrTmpDir) {
        return gstrTmpDir;
    }

    tmpSp = baseParams[BASE_TMPDIR].string_value;
    if ((tmpSp != NULL) && (stat(tmpSp, &stb) == 0) && (S_ISDIR(stb.st_mode))) {

        gstrTmpDir = copy_string(tmpSp);
        return gstrTmpDir;
    }

    tmpSp = getenv("TMPDIR");
    if ((tmpSp != NULL) && (stat(tmpSp, &stb) == 0) && (S_ISDIR(stb.st_mode))) {

        gstrTmpDir = copy_string(tmpSp);
        return gstrTmpDir;
    }

    if (gstrTmpDir == NULL) {
        gstrTmpDir = copy_string((char *)"/tmp");
    }

    return gstrTmpDir;
} // end function getmy_tmp_dir
